import os.path
from string import Template

import yaml
from selenium import webdriver
from selenium.webdriver import ActionChains  # 鼠标键盘行为
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from time import sleep

from selenium.webdriver.support.wait import WebDriverWait

from common.log import Logger

"""
页面基类: driver 的初始化，以及页面元素的二次封装
"""

logging = Logger(__name__).get_logger()


class BasePage:

    def __init__(self, driver=None):
        # 是 driver 的初始化
        # 初始化 driver对象
        if driver is None:
            service = Service(executable_path=r"./driver_version/chromedriver.exe")
            # 创建一个 webdriver 对象
            self.driver = webdriver.Chrome(service=service)
            self.driver.maximize_window()
            self.driver.implicitly_wait(10)
            # 打开浏览器，指定访问某个 url
            url = "https://deyunce:828123@mall.deyunce.com/"
            logging.info(f"当前访问的 url 是：{url}")
            self.driver.get(url)
        else:
            self.driver = driver

    # driver = None
    #
    # # 构造方法，打开浏览器，实现浏览器只打开一次
    # def __init__(self):
    #     if BasePage.driver is None:
    #         service = Service(executable_path=r"D:\WQ\deyunce class\practice\UiAuto60\drivers\chromedriver.exe")
    #         BasePage.driver = webdriver.Chrome(service=service)
    #
    #         # 最大化
    #         BasePage.driver.maximize_window()
    #         BasePage.driver.implicitly_wait(10)
    #         # 打开浏览器，指定访问某个 url
    #         url = "https://deyunce:828123@mall.deyunce.com/"
    #         logging.info(f"当前访问的 url 是：{url}")
    #         BasePage.driver.get(url)


    # 对元素定位与交互的二次封装
    # 查找单个元素
    def find(self, by, locator):  # by -> 用于接收定位方式的关键字如：xpath, id..., locator -> 用于接收表达式
        by_locator = None
        if by == 'id' or by == 'ID':
            by_locator = (By.ID, locator)
        elif by == 'class' or by == 'CLASS':
            by_locator = (By.CLASS_NAME, locator)
        elif by == 'name' or by == 'NAME':
            by_locator = (By.NAME, locator)
        elif by == 'css_selector' or by == 'CSS_SELECTOR':
            by_locator = (By.CSS_SELECTOR, locator)
        elif by == 'xpath' or by == 'XPATH':
            by_locator = (By.XPATH, locator)
        elif by == 'link_text' or by == 'LINK_TEXT':
            by_locator = (By.LINK_TEXT, locator)
        elif by == 'partial_link_text' or by == 'PARTIAL_LINK_TEXT':
            by_locator = (By.PARTIAL_LINK_TEXT, locator)
        elif by == 'tag_name' or by == 'TAG_NAME':
            by_locator = (By.TAG_NAME, locator)
        # ele = self.driver.find_element(*by_locator)  # * 代表了解包->即去掉元祖的括号，把元祖中的元素赋值给find_element 中的参数
        ele = WebDriverWait(self.driver, 10).until(lambda driver: driver.find_element(*by_locator), message="元素超时未找到")
        return ele  # 要把找到的元素返回出去供外部代码调用

    # 查找多个元素
    def finds(self, by, locator) -> list:
        by_locator = None
        if by == 'id' or by == 'ID':
            by_locator = (By.ID, locator)
        elif by == 'class' or by == 'CLASS':
            by_locator = (By.CLASS_NAME, locator)
        elif by == 'name' or by == 'NAME':
            by_locator = (By.NAME, locator)
        elif by == 'css_selector' or by == 'CSS_SELECTOR':
            by_locator = (By.CSS_SELECTOR, locator)
        elif by == 'xpath' or by == 'XPATH':
            by_locator = (By.XPATH, locator)
        elif by == 'link_text' or by == 'LINK_TEXT':
            by_locator = (By.LINK_TEXT, locator)
        elif by == 'partial_link_text' or by == 'PARTIAL_LINK_TEXT':
            by_locator = (By.PARTIAL_LINK_TEXT, locator)
        elif by == 'tag_name' or by == 'TAG_NAME':
            by_locator = (By.TAG_NAME, locator)
        # eles = self.driver.find_elements(*by_locator)  # * 代表了解包->即去掉元祖的括号，把元祖中的元素赋值给find_element 中的参数
        eles = WebDriverWait(self.driver, 10).until(lambda driver: driver.find_elements(*by_locator),
                                                    message="元素超时未找到")
        return eles  # 要把找到的元素返回出去供外部代码调用  eles -> list

    # 定位单个元素并进行输入
    def find_and_send(self, by, locator, text):
        self.find(by, locator).send_keys(text)

    # 定位单个元素并进行点击
    def find_and_click(self, by, locator):
        self.find(by, locator).click()  # 对页面元素进行交互，即会对页面进行点击操作（这是一个行为）

    # 定位单个元素进行清空
    def find_and_clear(self, by, locator):
        self.find(by, locator).clear()

    # 定位多个元素选择其中某个进行点击
    def finds_and_click(self, by, locator, index):
        self.finds(by, locator)[index].click()

    # 定位多个元素选取其中某个进行输入
    def finds_and_send(self, by, locator, index, text):
        self.finds(by, locator)[index].send_keys(text)

    # 定位多个元素选择其中某个进行清空
    def finds_and_clear(self, by, locator, index):
        self.finds(by, locator)[index].clear()

    # 把鼠标移动到某个元素上
    def move_to_ele(self, by, locator, index=None):
        if index is None:
            ele = self.find(by, locator)  # 获取要移动到的目标元素
        else:
            ele = self.finds(by, locator)[index]
        ActionChains(self.driver).move_to_element(ele).perform()

    # ★ 封装一个 run_steps 的方法，该方法用于实现基于关键字的行为驱动（数据来源于 yaml 文件）
    def run_steps(self, file_path, operation, **kwargs):
        """
         1.读取指定的 yaml 文件 -> 获取一个字典 res
         2.通过获取到的字典 res.get("operation") 获取到对应方法的所有行为列表 steps
         3.通过 yaml.dump()把上述数据进行保留格式的数据类型转换->转换成了yaml语法格式的str(通过 yaml语法保留了原数据类型)
         4.通过 Template().substitute()进行基于上述字符串的模板匹配，替换原字符串中有$开头的占位符-> yaml语法的格式的str
         5.通过 yaml.safe_load()把上述 yaml语法的字符串进行数据类型还原，还原成第一步中的 list(此时这个list已经匹配过$，传入了相关的参数)，把yaml语法的字符串类型还原成Python的数据类型
         6.通过 for 循环可迭代变量 step 遍历 steps  获取到一个字典{"action": "find_and_click", "locator":[ "xpath", "//*[@placeholder='请输入账号/手机号码']"],"index":None,"text":None}
         7.根据step 中的 action 进行 if-else  判断，如果 action 是  find_and_click  则执行  self.find_and_click 的交互
        :param file_path: 传入指定的 yaml 文件路径读取 yaml 文件
        :param operation: 传入方面名
        :return:
        """
        # 读取 yaml 文件
        # """
        # 1.读取yaml文件结合operation 获取到一个list [{'action':xxx,l....,text:$accountl,{'action':xxxx, text: $pwd}]
        # 2.通过 yaml.dump()把上述数据进行保留格式的数据类型转换->转换成了yaml语法格式的str(通过 yaml语法保留了原数据类型)
        # 3.通过 Template().substitute()进行基于上述字符串的模板匹配，替换原字符串中有$开头的占位符-> yaml语法的格式的str
        # 4.通过 yaml.safe_load()把上述 yaml语法的字符串进行数据类型还原，还原成第一步中的 list(此时这个list已经匹配过$，传入了相关的参数)
        # """

        with open(file_path, "r", encoding="UTF-8") as f:
            res = yaml.safe_load(f)  # 读取yaml文件内容，并转换成Python数据类型
        steps = res.get(operation)  # 注意：operation 会传入 方法名如：“click_login_button”
        # print(steps)
        # print(type(steps))
        yaml_str_steps = yaml.dump(steps)  # 把 Python 的数据类型转换成符合yaml语法的字符串类型
        # print("******经过 yaml.dump 转换后的 steps **********")
        # print(yaml_str_steps)
        # print(type(yaml_str_steps))
        # key_datas = {"account": "152000001", "pwd":'111111.'}
        if kwargs:
            for k in kwargs.keys():  # kwargs.keys()->["key1","key2"....]
                if isinstance(kwargs[k], str):
                    kwargs[k] = f"'{kwargs[k]}'"
            # 这里的for循环是为了防止yaml文件将字符串类型的数据在经过转换后变成整型、浮点型等输出，举例账号”11111“，未经处理转换后得到整型11111
            steps = Template(yaml_str_steps).substitute(kwargs)
            # 这里的Template().substitute()是为了把yaml_str_steps中的$account和$pwd替换成传入的值，并返回yaml语法的字符串类型
            # print("*****经过模板匹配后的字符串******")
            # print(steps)  # 此时这个 steps 是什么数据类型？->字符串
            # print(type(steps))
            # 把模板匹配后的 yaml 语法的字符串进行还原

            # # 确保 steps 是字符串类型
            # if isinstance(steps, list):
            #     steps = yaml.dump(steps)

            steps = yaml.safe_load(steps)
            # print("******还原后的 steps ******")
            # print(steps)
            # print(type(steps))

        for step in steps:
            # {'action': 'find_and_click', 'locator': ['xpath', "//*[text()='登录']"], 'index': None, 'text': None, 'sleep': 2}
            action = step.get("action")  # find_and_click
            locator = step.get("locator")  # ['xpath', "//*[text()='登录']"]
            sleep_time = step.get("sleep")  # 从对应的 action 中获取 sleep 对应的值，也就代表该 action 需要强制等待 sleep_time 再执行
            if sleep_time:
                sleep(sleep_time)
                logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素强制等待时间为：{sleep_time}")
            if action == "find":
                try:
                    ele = self.find(*locator)
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常")
                    return ele
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "find_and_click":
                try:
                    self.find_and_click(*locator)
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常")
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "find_and_send":
                try:
                    self.find_and_send(*locator, text=step.get("text"))
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常,输入的文本是{step.get('text')}")
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "find_and_clear":
                try:
                    self.find_and_clear(*locator)
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常")
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "finds":
                try:
                    eles = self.finds(*locator)
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常")
                    return eles
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "finds_and_click":
                try:
                    self.finds_and_click(*locator, index=step.get("index"))
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常,点击元素的index是：{step.get('index')}")
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "finds_and_send":
                try:
                    self.finds_and_send(*locator, index=step.get("index"), text=step.get("text"))
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常,元素交互的index是:{step.get('index')}"
                                 f"输入的文本是：{step.get('text')}")
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "finds_and_clear":
                try:
                    self.finds_and_clear(*locator, index=step.get("index"))
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常,元素交互的index是:{step.get('index')}")
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e
            elif action == "move_to_ele":
                try:
                    self.move_to_ele(*locator, index=step.get("index"))
                    logging.info(f"{file_path}:{operation}:{action}:{locator}定位元素正常")
                except Exception as e:
                    logging.error(f"{file_path}:{operation}:{action}:{locator} 元素定位异常，错误信息是：{e}")
                    raise e

    @staticmethod
    def get_path(relative_path: str) -> str:
        '''
        文件路径的拼接，返回一个绝对路径
        :param relative_path: 文件的相对路径
        :return: 文件的决定路径
        还是要根据具体的文件组成结构来看，以下情况是确保项目目录不变的情况下为前提设定的
        '''
        file_path = os.path.abspath(__file__)  # 获取当前文件的绝对路径
        # print(file_path)
        dir_path = os.path.dirname(file_path)  # 获取上级路径
        # print(dir_path)
        far_path = os.path.dirname(dir_path)  # 再网上推一层获取项目路径
        # print(far_path)
        path = far_path + relative_path
        return path


if __name__ == '__main__':
    # file_path = BasePage.get_path("/py_yaml/login_page.yaml")
    pass
    # b = BasePage(1)
    # b.run_steps(file_path, "use_right_account_login", account="152000001", pwd="111111.")
